درس 7 — Déployer

Appliquer, appeler, détruire

8 دقائق قراءة

هذا المحتوى غير مترجم بعد. تُعرض النسخة الأصلية.

Fin de lab un vendredi : terraform destroy répond « Destroy complete! », tout le monde rentre. Le lundi, la facture montre un dépôt Artifact Registry de 4 Go et un compte de service orphelin, créés à la main pendant un débogage et inconnus de l'état. Un destroy ne supprime que ce que Terraform connaît. Cette leçon crée réellement la plateforme de dev, l'appelle, la détruit, puis vérifie par labels et préfixes qu'il ne reste rien, y compris ce que Terraform ne connaît pas.

Le mécanisme

sequenceDiagram
  participant M as "make cloud-up"
  participant T as "Terraform (envs/dev)"
  participant AR as "Artifact Registry"
  participant CR as "Cloud Run"
  participant V as "Vertex AI"
  M->>T: apply 1 (image = null) : socle, registre, secret, IAM
  M->>AR: docker build + push, lecture du digest
  M->>T: apply 2 (image = …@sha256) : service + révision
  Note over CR: rôle aiplatform.user accordé il y a moins d'une minute
  M->>CR: make e2e : appel authentifié (jeton d'identité)
  CR->>V: generate_content
  V-->>CR: 403 tant que l'IAM n'est pas propagé, puis 200
  M->>T: make cloud-down : destroy dev, destroy bucket d'état
  M->>M: cloud-status : labels + préfixes, code 1 s'il reste quelque chose

Deux points font échouer un premier déploiement qui « marchait sur mon poste » :

  • La propagation IAM. Un rôle accordé par Terraform est enregistré tout de suite, mais la documentation Google annonce en général deux minutes avant qu'il soit effectif partout, et parfois sept minutes ou plus. Le premier appel du service à Vertex AI tombe dans cette fenêtre.
  • L'authentification de l'appelant. Le service est privé : aucun allUsers. make e2e présente un jeton d'identité (gcloud auth print-identity-token) ; votre compte a run.invoker par son rôle de projet. Sans jeton : 403, et c'est ce que le test vérifie en premier.

La mise en œuvre

files/Makefile (extrait)
cloud-up: state-up init ## Crée dev : socle, image, puis service
	@terraform -chdir=$(DEV) apply -input=false -auto-approve -no-color | tail -1
	@$(MAKE) --no-print-directory image TAG=$(TAG)
	@terraform -chdir=$(DEV) apply -input=false -auto-approve -no-color \
	  -var image=$$(cat .image) -var app_version=$(TAG) | tail -4

make image pousse avec un DOCKER_CONFIG temporaire (aucun identifiant ne reste dans ~/.docker) et écrit le digest dans .image. Le test de bout en bout (e2e.py) réessaie les réponses 502 et 503 pendant 300 s au plus : 502 couvre la propagation IAM (le service reçoit un 403 de Vertex AI), 503 un quota partagé épuisé. Au-delà, il échoue.

cd files
make cloud-up PROJECT=<votre-projet> TICKETING_TOKEN=<jeton>
make e2e PROJECT=<votre-projet>
make cloud-down PROJECT=<votre-projet> TICKETING_TOKEN=<jeton>

Les variables passées en ligne de commande l'emportent sur .env : le Makefile ne lit dans .env que GOOGLE_CLOUD_PROJECT et TICKETING_TOKEN, sans l'exporter.

make cloud-status ne lit pas l'état Terraform : il interroge Google Cloud par label (formation-lab=plateforme-ia-modulaire) et par préfixe (lab-pia) sur huit types de ressources, liaisons IAM du projet comprises. C'est ce qui attrape les ressources créées à la main.

Résultat obtenu (2026-10-16)

$ make cloud-up        # 56 s
state_bucket = "lab-pia-tfstate-…"
image : europe-west1-docker.pkg.dev/<projet>/lab-pia-dev-images/triage@sha256:9a662e38f069…53d1cc
archive_bucket = "lab-pia-dev-archive-8cb630"
registry_url = "europe-west1-docker.pkg.dev/<projet>/lab-pia-dev-images"
service_name = "lab-pia-dev-triage"
service_uri = "https://lab-pia-dev-triage-<hash>-ew.a.run.app"

$ make e2e             # 63 s
sans jeton      -> HTTP 403
/health         -> {'status': 'ok', 'version': '20261016-152533', 'model': 'gemini-2.5-flash', 'ticketing_token_loaded': True}
 56528 ms  {'categorie': 'incident', 'priorite': 'P1', 'equipe': 'infra', 'resume': "Scanner quai 4 HS, 30 camions bloqués, impact majeur d'opérations de déchargement."}
  2535 ms  {'categorie': 'acces', 'priorite': 'P2', 'equipe': 'support-poste', 'resume': "Accès au dossier partagé Achats impossible depuis le nouveau poste de l'utilisateur."}
  2141 ms  {'categorie': 'achat', 'priorite': 'P3', 'equipe': 'achats', 'resume': "Commande de deux écrans 27 pouces pour l'équipe planification."}
E2E OK

Les 56 secondes du premier appel sont l'attente de la propagation IAM. Les journaux du service le montrent, avec le filtre même de la métrique lab-pia-dev/errors :

$ gcloud logging read 'resource.labels.service_name="lab-pia-dev-triage" AND severity>=ERROR'
13:18:54  403  ClientError  403 PERMISSION_DENIED. Permission 'aiplatform.endpoints.predict' denied …
13:19:04  403  ClientError  403 PERMISSION_DENIED. …
13:19:14  403  ClientError  403 PERMISSION_DENIED. …

Le premier cycle du 2026-10-15 avait donné le même motif (71 s, avec en plus un 429 de quota). Juste après, un plan sur dev répond No changes. Your infrastructure matches the configuration., et la valeur du jeton est absente de l'état distant (grep -c : 0).

Puis la destruction :

$ make cloud-down      # 55 s
Destroy complete! Resources: 14 destroyed.
Destroy complete! Resources: 2 destroyed.
RESTE Comptes de service : lab-pia-dev-run@<projet>.iam.gserviceaccount.com
make[1]: *** [Makefile:76 : cloud-status] Erreur 1
Aucune ressource plateforme-ia-modulaire restante.

La ligne RESTE n'est pas une fuite : le compte de service venait d'être supprimé, et la liste IAM est à cohérence différée. La deuxième vérification, 15 s plus tard, est vide. C'est pour cela que cloud-down revérifie trois fois avant d'échouer.

Premier cycle complet (2026-10-15) : cloud-up, e2e, puis les erreurs journalisées pendant la propagation IAM

Coût de ce cycle : trois appels gemini-2.5-flash de quelques centaines de jetons, une image de quelques dizaines de Mo stockée quelques minutes, Cloud Run à zéro instance au repos. De l'ordre du centime.

Ce qui casse en production

  • 403 au premier appel après un déploiement. Symptôme : PERMISSION_DENIED sur un rôle pourtant visible dans la console. Cause : propagation IAM. Parade : accorder les rôles dans un apply antérieur au déploiement du service (c'est le cas ici : apply 1 puis apply 2, mais l'écart n'est que de 30 s) ; côté client, réessayer avec un délai borné.
  • destroy qui laisse des ressources. Tout ce qui a été créé hors Terraform (image poussée, compte de débogage, bucket de test) survit. Parade : une vérification par label et préfixe, indépendante de l'état, qui échoue s'il reste quelque chose.
  • Vérification trop rapide. À l'inverse, une liste lue juste après une suppression peut encore afficher la ressource. Parade : revérifier après un délai avant de conclure.
  • Sortie du modèle non déterministe. Même avec temperature = 0, la demande d'accès au dossier Achats a été classée incident lors de deux cycles et acces lors d'un troisième. Le test vérifie donc la priorité, pas la catégorie, et seulement sur les cas sans ambiguïté (P1, P3). Un test de bout en bout de LLM affirme des invariants, pas des chaînes exactes.
  • Le bucket d'état détruit avant l'environnement. On perd la trace de ce qui existe. Parade : l'ordre de cloud-down (dev d'abord, bucket d'état ensuite), et en vrai, un bucket d'état qu'on ne détruit jamais.

La règle de décision

Un environnement éphémère se crée et se détruit par la même commande, et sa destruction se prouve par une vérification indépendante de l'état. En prod, on ne détruit pas : deletion_policy = "PREVENT" fait échouer toute tentative, et c'est voulu. Surveillez le délai entre l'octroi d'un rôle et son premier usage ; s'il est de quelques secondes, prévoyez l'échec.

Pratique

Juste après make cloud-up, le premier appel à /v1/triage répond 502 ; les journaux montrent 403 PERMISSION_DENIED sur aiplatform.endpoints.predict. Le plan montre bien roles/aiplatform.user sur le compte run. Que faites-vous ?

make cloud-down affiche « Destroy complete! » puis « RESTE Buckets : lab-pia-debug-logs ». Ce bucket n'est pas dans le code. Quelle est la bonne réaction ?

Exercice. Ajoutez à make cloud-status la vérification des images restantes dans les dépôts Artifact Registry du lab, au cas où un dépôt aurait été recréé à la main.

Voir la solution
	chk "Images" bash -c 'for r in $$(gcloud artifacts repositories list --project $(PROJECT) --location $(REGION) --filter="name~/lab-pia" --format="value(name)"); do gcloud artifacts docker images list $(REGION)-docker.pkg.dev/$(PROJECT)/$$(basename $$r) --format="value(package)"; done'; \

La ligne s'insère avant le test final de cloud-status. Un dépôt supprimé emporte ses images : la vérification ne sert que si un dépôt existe encore, ce que la ligne « Artifact Registry » signale déjà. Elle précise ce qu'il contient, donc ce qu'il coûte.

  • Je déploie un service en deux applications : socle, puis service avec une image par digest.
  • Je teste un service privé avec un jeton d'identité, et je vérifie qu'il refuse un appel anonyme.
  • Je sais reconnaître un 403 de propagation IAM et le distinguer d'un droit manquant.
  • Je détruis un environnement et je prouve, par labels et préfixes, qu'il ne reste rien.